package com.example.auth.service;

import com.example.auth.dto.ChatMessageDTO;
import com.example.auth.dto.ConversationDTO;
import com.example.auth.entity.ChatMessage;
import com.example.auth.entity.Conversation;
import com.example.auth.entity.User;
import com.example.auth.repository.ChatMessageRepository;
import com.example.auth.repository.ConversationRepository;
import com.example.auth.repository.UserRepository;
import jakarta.transaction.Transactional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class ChatService {

    private final ChatMessageRepository chatMessageRepository;
    private final ConversationRepository conversationRepository;
    private final UserRepository userRepository;
    private final SimpMessagingTemplate messagingTemplate;

    public ChatService(
            ChatMessageRepository chatMessageRepository,
            ConversationRepository conversationRepository,
            UserRepository userRepository,
            SimpMessagingTemplate messagingTemplate
    ) {
        this.chatMessageRepository = chatMessageRepository;
        this.conversationRepository = conversationRepository;
        this.userRepository = userRepository;
        this.messagingTemplate = messagingTemplate;
    }

    /**
     * Retrieves all conversations for a given user.
     * @param currentUser The user for whom to retrieve conversations.
     * @return A list of ConversationDTO objects, each representing a conversation.
     */
    public List<ConversationDTO> getConversationsForUser(User currentUser) {
        List<Conversation> conversations = conversationRepository.findByUserOrderByLastMessageTimeDesc(currentUser);

        return conversations.stream()
                .map(conversation -> new ConversationDTO(conversation, currentUser))
                .collect(Collectors.toList());
    }

    /**
     * Retrieves or creates a conversation between two users. If a conversation
     * already exists, it retrieves the existing one. Otherwise, it creates a new conversation.
     * @param user1 The first user in the conversation.
     * @param user2 The second user in the conversation.
     * @return A ConversationDTO representing the conversation, including recent messages.
     */
    @Transactional
    public ConversationDTO getOrCreateConversation(User user1, User user2) {
        Optional<Conversation> existingConversation = conversationRepository.findByUsers(user1, user2);

        Conversation conversation;
        if (existingConversation.isPresent()) {
            conversation = existingConversation.get();
        } else {
            conversation = new Conversation();
            conversation.setUser1(user1);
            conversation.setUser2(user2);
            conversation = conversationRepository.save(conversation);
        }

        Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "sentTime"));
        Page<ChatMessage> messages = chatMessageRepository.findByConversationOrderBySentTimeDesc(conversation, pageable);

        List<ChatMessageDTO> messageDTOs = messages.getContent().stream()
                .sorted((m1, m2) -> m1.getSentTime().compareTo(m2.getSentTime()))
                .map(ChatMessageDTO::new)
                .collect(Collectors.toList());

        return new ConversationDTO(conversation, user1, messageDTOs);
    }

    /**
     * Sends a chat message from one user to another.
     * @param senderId The ID of the user sending the message.
     * @param recipientId The ID of the user receiving the message.
     * @param content The text content of the message.
     * @return A ChatMessageDTO representing the sent message.
     */
    @Transactional
    public ChatMessageDTO sendMessage(Long senderId, Long recipientId, String content) {
        return sendMessage(senderId, recipientId, content, null, null, null, null);
    }

    /**
     * Sends a chat message with optional file attachment from one user to another.
     * @param senderId The ID of the user sending the message.
     * @param recipientId The ID of the user receiving the message.
     * @param content The text content of the message.
     * @param fileUrl The URL of the attached file (optional).
     * @param fileName The name of the attached file (optional).
     * @param fileType The type of the attached file (optional).
     * @param fileSize The size of the attached file (optional).
     * @return A ChatMessageDTO representing the sent message.
     * @throws IllegalArgumentException If senderId or recipientId is null, or if content is null/empty (unless a file is attached).
     * @throws RuntimeException If the sender or recipient user is not found or is inactive.
     */
    @Transactional
    public ChatMessageDTO sendMessage(Long senderId, Long recipientId, String content,
                                      String fileUrl, String fileName, String fileType, Long fileSize) {
        try {
            System.out.println("ChatService.sendMessage() - senderId: " + senderId + ", recipientId: " + recipientId);

            if (senderId == null) {
                throw new IllegalArgumentException("Sender ID cannot be null");
            }

            if (recipientId == null) {
                throw new IllegalArgumentException("Recipient ID cannot be null");
            }

            if (content == null || content.trim().isEmpty()) {
                // If it is a file message, allow the content to be empty
                if (fileUrl == null) {
                    throw new IllegalArgumentException("Message content cannot be empty");
                } else {
                    content = ""; // Set to an empty string to avoid database constraint issues
                }
            }

            // Find the sender - enhance error handling and logging
            Optional<User> senderOpt = userRepository.findById(senderId);
            if (!senderOpt.isPresent()) {
                String errorMsg = "Sender does not exist (ID: " + senderId + ")";
                System.err.println("Failed to send message: Could not find sender with ID " + senderId);
                throw new RuntimeException(errorMsg);
            }
            User sender = senderOpt.get();

            // Check sender status
            if (!"active".equals(sender.getStatus())) {
                String errorMsg = "Sender account is not active (ID: " + senderId + ")";
                System.err.println("Failed to send message: " + errorMsg);
                throw new RuntimeException(errorMsg);
            }

            System.out.println("Found sender: " + sender.getUsername() + " (ID: " + sender.getId() + ")");

            // Find the recipient
            Optional<User> recipientOpt = userRepository.findById(recipientId);
            if (!recipientOpt.isPresent()) {
                String errorMsg = "Recipient does not exist (ID: " + recipientId + ")";
                System.err.println("Failed to send message: Could not find recipient with ID " + recipientId);
                throw new RuntimeException(errorMsg);
            }
            User recipient = recipientOpt.get();

            // Check recipient status
            if (!"active".equals(recipient.getStatus())) {
                String errorMsg = "Recipient account is not active (ID: " + recipientId + ")";
                System.err.println("Failed to send message: " + errorMsg);
                throw new RuntimeException(errorMsg);
            }

            System.out.println("Found recipient: " + recipient.getUsername() + " (ID: " + recipient.getId() + ")");

            // Get or create conversation
            System.out.println("Finding or creating conversation");
            Optional<Conversation> optionalConversation = conversationRepository.findByUsers(sender, recipient);
            Conversation conversation;

            if (optionalConversation.isPresent()) {
                conversation = optionalConversation.get();
                System.out.println("Using existing conversation, ID: " + conversation.getId());
            } else {
                conversation = new Conversation();
                conversation.setUser1(sender);
                conversation.setUser2(recipient);
                conversation = conversationRepository.save(conversation);
                System.out.println("Created a new conversation, ID: " + conversation.getId());
            }

            // Create message
            ChatMessage message = new ChatMessage();
            message.setSender(sender);
            message.setRecipient(recipient);
            message.setContent(content);
            message.setRead(false);
            message.setConversation(conversation);

            // Set file related information (if any)
            if (fileUrl != null) {
                message.setFileUrl(fileUrl);
                message.setFileName(fileName);
                message.setFileType(fileType);
                message.setFileSize(fileSize);
                System.out.println("Message contains file: " + fileName + " (" + fileType + ")");
            }

            // Save message
            message = chatMessageRepository.save(message);
            System.out.println("Saved message, ID: " + message.getId());

            // Update the conversation's unread message count and last message time
            if (conversation.getUser1().equals(recipient)) {
                conversation.setUnreadCountUser1(conversation.getUnreadCountUser1() + 1);
            } else {
                conversation.setUnreadCountUser2(conversation.getUnreadCountUser2() + 1);
            }
            conversation.setLastMessageTime(message.getSentTime());
            conversationRepository.save(conversation);
            System.out.println("Updated conversation's unread message count and last message time");

            // Create DTO
            ChatMessageDTO messageDTO = new ChatMessageDTO(message);

            // Send the message to the recipient and sender via WebSocket
            // Send to recipient
            messagingTemplate.convertAndSendToUser(
                    recipient.getId().toString(),
                    "/queue/messages",
                    messageDTO
            );

            // Send a copy to the sender
            messagingTemplate.convertAndSendToUser(
                    sender.getId().toString(),
                    "/queue/messages",
                    messageDTO
            );

            System.out.println("Message has been pushed to the sender and recipient via WebSocket");

            return messageDTO;

        } catch (Exception e) {
            System.err.println("An exception occurred while sending the message: " + e.getMessage());
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * Retrieves messages for a specific conversation, paginated and sorted by sent time.
     * @param conversationId The ID of the conversation for which to retrieve messages.
     * @param page The page number (starting from 0).
     * @param size The number of messages per page.
     * @return A list of ChatMessageDTO objects representing the messages in the conversation.
     * @throws RuntimeException If the conversation does not exist.
     */
    public List<ChatMessageDTO> getMessagesForConversation(Long conversationId, int page, int size) {
        Conversation conversation = conversationRepository.findById(conversationId)
                .orElseThrow(() -> new RuntimeException("Conversation does not exist"));

        Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "sentTime"));
        Page<ChatMessage> messages = chatMessageRepository.findByConversationOrderBySentTimeDesc(conversation, pageable);

        return messages.getContent().stream()
                .sorted((m1, m2) -> m1.getSentTime().compareTo(m2.getSentTime()))
                .map(ChatMessageDTO::new)
                .collect(Collectors.toList());
    }

    /**
     * Marks all messages in a conversation as read for a specific user.
     * @param conversationId The ID of the conversation to mark as read.
     * @param userId The ID of the user for whom to mark messages as read.
     * @return The number of messages that were marked as read.
     * @throws RuntimeException If the conversation or user does not exist.
     */
    @Transactional
    public int markConversationAsRead(Long conversationId, Long userId) {
        Conversation conversation = conversationRepository.findById(conversationId)
                .orElseThrow(() -> new RuntimeException("Conversation does not exist"));

        User user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User does not exist"));

        int updatedCount = chatMessageRepository.markAllAsRead(conversation, user);

        // Reset the unread count for the conversation
        if (conversation.getUser1().getId().equals(userId)) {
            conversation.setUnreadCountUser1(0);
        } else if (conversation.getUser2().getId().equals(userId)) {
            conversation.setUnreadCountUser2(0);
        }

        conversationRepository.save(conversation);

        return updatedCount;
    }

    /**
     * Retrieves messages in a conversation that were sent before a specific message ID.
     * Useful for implementing infinite scrolling or loading older messages.
     * @param conversationId The ID of the conversation.
     * @param messageId The ID of the message to retrieve messages before.
     * @param size The number of messages to retrieve.
     * @return A list of ChatMessageDTO objects representing the older messages.
     * @throws RuntimeException If the conversation does not exist.
     */
    public List<ChatMessageDTO> getMessagesBeforeId(Long conversationId, Long messageId, int size) {
        Conversation conversation = conversationRepository.findById(conversationId)
                .orElseThrow(() -> new RuntimeException("Conversation does not exist"));

        Pageable pageable = PageRequest.of(0, size, Sort.by(Sort.Direction.DESC, "sentTime"));
        Page<ChatMessage> messages = chatMessageRepository.findOlderMessages(conversation, messageId, pageable);

        return messages.getContent().stream()
                .sorted((m1, m2) -> m1.getSentTime().compareTo(m2.getSentTime()))
                .map(ChatMessageDTO::new)
                .collect(Collectors.toList());
    }

    /**
     * Retrieves the number of unread messages for a specific user across all conversations.
     * @param userId The ID of the user.
     * @return The number of unread messages.
     * @throws RuntimeException If the user does not exist.
     */
    public int getUnreadMessageCount(Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User does not exist"));

        return chatMessageRepository.countUnreadMessagesByUser(user);
    }

    /**
     * Retrieves the number of conversations with unread messages for a specific user.
     * @param userId The ID of the user.
     * @return The number of conversations with unread messages.
     * @throws RuntimeException If the user does not exist.
     */
    public int getUnreadConversationCount(Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User does not exist"));

        return conversationRepository.countConversationsWithUnreadMessages(user);
    }

    /**
     * Sends an error notification to a user via WebSocket.  This allows real-time
     * error reporting to the client.
     * @param userId The ID of the user to send the notification to.
     * @param errorMessage The error message to send.
     */
    public void sendErrorNotification(Long userId, String errorMessage) {
        if (userId == null) {
            System.err.println("Cannot send error notification: User ID is null");
            return;
        }

        try {
            Map<String, Object> errorNotification = new HashMap<>();
            errorNotification.put("error", true);
            errorNotification.put("message", errorMessage);

            messagingTemplate.convertAndSendToUser(
                    userId.toString(),
                    "/queue/errors",
                    errorNotification
            );

            System.out.println("Sent error notification to user " + userId + ": " + errorMessage);
        } catch (Exception e) {
            System.err.println("Failed to send error notification: " + e.getMessage());
        }
    }
}